﻿using Furion;
using Furion.DatabaseAccessor;
using FurStudy.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Query;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace FurStudy.EntityFramework.Core
{
    /// <summary>
    /// Tips：Furion推荐使用此方式配置数据库连接字符串
    /// </summary>
    [AppDbContext("DbConnectionString")] // 支持 `appsetting.json` 名或 连接字符串
    public class FurionDbContext : AppDbContext<FurionDbContext>, IMultiTenantOnTable, IModelBuilderFilter // 继承 AppDbContext<>类
    {
        // 表值函数配置
        public IQueryable<F_Person> GetPersons(int id) => FromExpression(() => GetPersons(id));

        /// <summary>
        /// 基于TenantId的方式
        /// 此方式在中小型企业系统中最为常用，维护成本低，购置成本低
        /// </summary>
        /// <returns></returns>
        public object GetTenantId()
        {
            // base.Tenant只是Furion框架提供的默认租户实现方法，如果不能满足业务需求，只需要在GetTenantId里面写你的业务代码即可，也就是无需调用base.Tenant

            // 这里是你获取TenantId的逻辑
            //return 你的 TenantId;

            return base.Tenant?.TenantId ?? Guid.Empty; // Furion默认
        }

        /// <summary>
        /// 基于Database的方式
        /// 此方式在中大型企业系统中最为常用，一个租户（客户）一个独立的数据库
        /// </summary>
        /// <returns></returns>
        public string GetDatabaseConnectionString()
        {
            // base.Tenant只是Furion框架提供的默认租户实现方法，如果不能满足业务需求，只需要在GetDatabaseConnectionString里面写你的业务代码即可，也就是无需调用base.Tenant。

            // 这里是你获取DatabaseConnecionString的逻辑
            //return 你的 连接字符串;

            return base.Tenant?.ConnectionString ?? "默认链接字符串";
        }

        /// <summary>
        /// 基于Schema的方式
        /// 此方式在中小型企业系统中也不少见，一个租户（客户）共享数据库且不同Schema
        /// </summary>
        /// <returns></returns>
        public string GetSchemaName()
        {
            return base.Tenant?.Schema ?? "dbo";
        }
        // 关于Code First数据迁移
        // 基于Schema方式比较特别，生成数据迁移的时候没办法获取租户信息，所以建议分开多次迁移
        public string GetSchemaName1()
        {
            return base.Tenant?.Schema ?? "租户一Schema";
        }
        public string GetSchemaName2()
        {
            return base.Tenant?.Schema ?? "租户二Schema";
        }

        /// <summary>
        /// 继承父类的构造函数
        /// </summary>
        /// <param name="options"></param>
        public FurionDbContext(DbContextOptions<FurionDbContext> options) : base(options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("数据库连接字符串");

            // SqlServer低版本支持动态配置
            // 正常情况下，只需要在注册的时候指定@2008数据库即可，有些时候我们可能需要在DbContext的OnConfigure中配置，这个时候就需要添加以下代码
            optionsBuilder.UseSqlServer(DbProvider.GetConnectionString<FurOtherDbContext>())
                .ReplaceService<IQueryTranslationPostprocessorFactory, SqlServer2008QueryTranslationPostprocessorFactory>();

            // 配置实体懒加载
            optionsBuilder.UseLazyLoadingProxies()
                .UseSqlite(DbProvider.GetConnectionString<DefaultDbContext>());

            base.OnConfiguring(optionsBuilder);

            // 在需要多租户的数据库上下文中重写 OnConfiguring 方法并配置连接字符串：
            optionsBuilder.UseSqlite(GetDatabaseConnectionString());
            base.OnConfiguring(optionsBuilder);
            // 特别注意：基于Database方式做Code First的时候，需要手动指定迁移程序名称 如：
            optionsBuilder.UseSqlite(GetDatabaseConnectionString(), options =>
            {
                options.MigrationsAssembly("My.Migrations");
            });
        }

        // 特别注意：这三种方式可以同时使用，但是有优先级：[AppDbContext] -> 在注册数据库服务时配置 -> OnConfiguring（低到高）
        // 也就是[OnConfiguring]配置会覆盖 在[注册数据库服务时配置]配置，在[注册数据库服务时配置]配置会覆盖[AppDbContext]配置所配置的连接字符串

        /// <summary>
        /// 表值函数配置
        /// </summary>
        /// <param name="modelBuilder"></param>
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity(typeof(F_Person)).HasNoKey();
            modelBuilder.HasDbFunction(() => GetPersons(default));
        }

        /// <summary>
        /// 重写保存之前事件
        /// 新增数据的时候自动设置TenantId的值
        /// 更新数据的时候排除TenantId属性更新
        /// </summary>
        /// <param name="eventData"></param>
        /// <param name="result"></param>
        protected override void SavingChangesEvent(DbContextEventData eventData, InterceptionResult<int> result)
        {
            // 获取当前事件对应上下文
            var dbContext = eventData.Context;

            // 强制重新检查一遍实体更改信息
            // dbContext.ChangeTracker.DetectChanges();

            // 获取所有更改、删除、新增实体，但排除审计实体（避免死循环）
            var entities = dbContext.ChangeTracker.Entries()
                .Where(t => t.Entity.GetType() != typeof(Audit) && (t.State == EntityState.Modified || t.State == EntityState.Deleted || t.State == EntityState.Added))
                .ToList();

            // 通过请求中获取当前操作人
            var userId = App.GetService<IHttpContextAccessor>().HttpContext.Items["UserId"];

            // 获取所有已更改的实体
            foreach (var entity in entities)
            {
                // 获取实体类型
                var entityType = entity.Entity.GetType();

                // 获取所有实体有效属性，排除[NotMapper]属性
                var props = entity.OriginalValues.Properties;

                // 获取实体当前（现在）的值
                var currentValues = entity.CurrentValues;

                // 获取数据库中实体的值
                var databaseValues = entity.GetDatabaseValues();

                // 遍历所有属性
                foreach (var prop in props)
                {
                    // 获取属性名
                    var propName = prop.Name;

                    // 获取现在的实体值
                    var newValue = currentValues[propName];

                    object oldValue = null;
                    // 如果是新增数据，则databaseValues为空，所以需要判断一下
                    if (databaseValues != null)
                    {
                        oldValue = databaseValues[propName];
                    }

                    // 插入审计日志表，Audit是自定义的实体
                    dbContext.Set<Audit>().Add(new Audit
                    {
                        Table = entityType.Name,            // 表名
                        Column = propName,                  // 更新的列
                        NewValue = newValue,                // 新值
                        OldValue = oldValue,                // 旧值
                        CreatedTime = DateTime.Now,         // 操作时间
                        UserId = userId,                    // 操作人
                        Operate = entity.State.ToString(),  // 操作方式：新增、更新、删除
                    });

                    // Tip：如果对性能有所要求，那么建议审计日志通过日志组件写入数据库，如通过Nlog、Log4Net这些等：
                    //logger.Information(JsonConvert.SerializeObject(new Audit
                    //{
                    //    Table = entityType.Name,    // 表名
                    //    Column = propName,  // 更新的列
                    //    newValue = newValue,    // 新值
                    //    OldValue = oldValue,    // 旧值
                    //    CreatedTime = DateTime.Now, // 操作时间
                    //    UserId = userId,    // 操作人
                    //    Operate = entity.State.ToString();  // 操作方式：新增、更新、删除
                    //}));

                    // 通过上面的例子，就可以对数据库所有的新增、更新、删除进行监控了。
                }
            }


            #region SavedChangesEvent拦截 -- 自动增加租户Id

            // 获取所有新增和更新的实体
            var entitiesWithIntercept = dbContext.ChangeTracker.Entries()
                .Where(t => t.State == EntityState.Added || t.State == EntityState.Modified);

            foreach (var entity in entitiesWithIntercept)
            {
                switch (entity.State)
                {
                    // 自动设置组合Id
                    case EntityState.Added:
                        entity.Property(nameof(Entity.TenantId)).CurrentValue = GetTenantId();
                        break;
                    // 排除租户Id
                    case EntityState.Modified:
                        entity.Property(nameof(Entity.TenantId)).IsModified = false;
                        break;
                    default:
                        break;
                }
            }

            #endregion

            //base.SavingChangesEvent(eventData, result);
        }

        /// <summary>
        /// 实现 IModelBuilderFilter 接口
        /// IModelBuilderFilter接口是全局查询过滤器实现接口，所以我们需要配置实体TenantId过滤器
        /// </summary>
        /// <param name="modelBuilder"></param>
        /// <param name="entityBuilder"></param>
        /// <param name="dbContext"></param>
        /// <param name="dbContextLocator"></param>
        public void OnCreating(ModelBuilder modelBuilder, EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator)
        {
            // 设置软删除表达式
            var fakeDeleteQueryFilterExpression = FakeDeleteQueryFilterExpression(entityBuilder, dbContext);
            if (fakeDeleteQueryFilterExpression == null) return;

            entityBuilder.HasQueryFilter(fakeDeleteQueryFilterExpression);

            entityBuilder.HasQueryFilter(BuildTenantQueryFilter(entityBuilder, dbContext));
        }

        public LambdaExpression FakeDeleteQueryFilterExpression(EntityTypeBuilder entityBuilder, DbContext dbContext)
        {
            return null;
        }
    }
}
